<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Canvas Thread — Space.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="assets/css/style.css">

    <script type="module">
        import { Thread, ticker } from '../src/index.js';

        // Based on https://codepen.io/zepha/pen/VpXvBJ

        class CanvasNoise {
            constructor(params) {
                this.params = params;

                this.initParameters();
                this.initCanvas();
            }

            initParameters() {
                const defaults = {
                    width: 1,
                    height: 1,
                    tileSize: 250,
                    monochrome: true
                };

                this.params = Object.assign(defaults, this.params);
            }

            initCanvas() {
                this.canvas = this.params.canvas;
                this.canvas.width = this.params.width;
                this.canvas.height = this.params.height;
                this.context = this.canvas.getContext('2d');

                this.tile = typeof window === 'undefined' ? new OffscreenCanvas(this.params.tileSize, this.params.tileSize) : document.createElement('canvas');
                this.tile.width = this.params.tileSize;
                this.tile.height = this.params.tileSize;
                this.tileContext = this.tile.getContext('2d');
            }

            // Public methods

            resize = (width, height, dpr) => {
                this.canvas.width = Math.round(width * dpr);
                this.canvas.height = Math.round(height * dpr);

                this.tile.width = Math.round(this.params.tileSize * dpr);
                this.tile.height = Math.round(this.params.tileSize * dpr);

                this.width = this.canvas.width / this.tile.width + 1; // One extra tile for row offset
                this.height = this.canvas.height / this.tile.height;

                this.update();
            };

            update = () => {
                const pixels = new ImageData(this.tile.width, this.tile.height);

                for (let i = 0, l = pixels.data.length; i < l; i += 4) {
                    const rand = 255 * Math.random();

                    pixels.data[i] = this.params.monochrome ? rand : 255 * Math.random();
                    pixels.data[i + 1] = this.params.monochrome ? rand : 255 * Math.random();
                    pixels.data[i + 2] = this.params.monochrome ? rand : 255 * Math.random();
                    pixels.data[i + 3] = 255;
                }

                this.tileContext.putImageData(pixels, 0, 0);

                for (let x = 0, xl = this.width; x < xl; x++) {
                    for (let y = 0, yl = this.height; y < yl; y++) {
                        this.context.drawImage(this.tile, x * this.tile.width - (y % 2 === 0 ? this.tile.width / 2 : 0), y * this.tile.height, this.tile.width, this.tile.height);
                    }
                }
            };
        }

        class CanvasNoiseThread {
            constructor() {
                this.addListeners();
            }

            addListeners() {
                addEventListener('message', this.onMessage);

                ticker.start();
            }

            // Event handlers

            onMessage = ({ data }) => {
                this[data.message.fn].call(this, data.message);
            };

            onUpdate = () => {
                this.noise.update();
            };

            // Public methods

            init = ({ params }) => {
                this.noise = new CanvasNoise(params);
            };

            resize = ({ width, height, dpr }) => {
                this.noise.resize(width, height, dpr);
            };

            start = ({ fps }) => {
                ticker.add(this.onUpdate, fps);
            };

            stop = () => {
                ticker.remove(this.onUpdate);
            };
        }

        class CanvasNoiseController {
            static init(params) {
                this.params = params;

                this.initThread();
            }

            static initThread() {
                if ('transferControlToOffscreen' in this.params.canvas && !/firefox/i.test(navigator.userAgent)) {
                    this.thread = new Thread({
                        imports: [
                            ['../src/index.js', 'ticker']
                        ],
                        classes: [CanvasNoise],
                        controller: [CanvasNoiseThread, 'init', 'resize', 'start', 'stop']
                    });

                    this.element = this.params.canvas;
                    this.params.canvas = this.element.transferControlToOffscreen();

                    this.thread.init({ params: this.params, buffer: [this.params.canvas] });
                } else {
                    ticker.start();

                    this.noise = new CanvasNoise(this.params);
                }
            }

            // Event handlers

            static onUpdate = () => {
                this.noise.update();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                if (this.thread) {
                    this.thread.resize({ width, height, dpr });
                } else {
                    this.noise.resize(width, height, dpr);
                }
            };

            static start = () => {
                if (this.thread) {
                    this.thread.start({ fps: 20 });
                } else {
                    ticker.add(this.onUpdate, 20);
                }
            };

            static stop = () => {
                if (this.thread) {
                    this.thread.stop();
                } else {
                    ticker.remove(this.onUpdate);
                }
            };
        }

        class App {
            static async init() {
                this.initCanvas();
                this.initControllers();

                this.addListeners();
                this.onResize();
            }

            static initCanvas() {
                this.canvas = document.createElement('canvas');
                document.body.appendChild(this.canvas);
            }

            static initControllers() {
                CanvasNoiseController.init({ canvas: this.canvas });
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                window.addEventListener('load', this.onLoad);
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                this.canvas.style.width = width + 'px';
                this.canvas.style.height = height + 'px';

                CanvasNoiseController.resize(width, height, dpr);
            };

            static onLoad = () => {
                CanvasNoiseController.start();
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
